/* ------------------------------------------------------------------
 * Copyright (C) 2011 Martin Storsjo
 * Copyright (C) 2022 Matthias P. Braendli
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 * -------------------------------------------------------------------
 */

/*! \mainpage Introduction
 *  The ODR-mmbTools ODR-AudioEnc Audio encoder can encode audio for
 *  ODR-DabMux, both DAB and DAB+. The DAB encoder is based on toolame. The
 *  DAB+ encoder requires a the Fraunhofer FDK AAC library, with the
 *  necessary patches for 960-transform to do DAB+ broadcast encoding.
 *
 *  This document describes some internals of the encoder, and is intended
 *  to help developers understand and improve the software package.
 *
 *  User documentation is available in the README and in the ODR-mmbTools
 *  Guide, available on the www.opendigitalradio.org website.
 *
 *  The readme for the whole package is \ref md_README
 *
 *  Interesting starting points for the encoder
 *  - \ref odr-audioenc.cpp Main encoder file
 *  - \ref VLCInput.h VLC Input
 *  - \ref GSTInput.h GST Input
 *  - \ref AlsaInput.h Alsa Input
 *  - \ref JackInput.h JACK Input
 *  - \ref Outputs.h ZeroMQ, file and EDI outputs
 *  - \ref SampleQueue.h
 *  - \ref charset.h Charset conversion
 *  - \ref toolame.h libtolame API
 *  - \ref AudioLevel
 *  - \ref DataInput
 *  - \ref SilenceDetection
 *
 *  \file odr-audioenc.cpp
 *  \brief The main file for the audio encoder
 */

#include "config.h"
#include "PadInterface.h"
#include "AlsaInput.h"
#include "FileInput.h"
#include "JackInput.h"
#include "VLCInput.h"
#include "GSTInput.h"
#include "SampleQueue.h"
#include "AACDecoder.h"
#include "StatsPublish.h"
#include "Outputs.h"
#include "common.h"
#include "wavfile.h"
#include "utils.h"

extern "C" {
#include "encryption.h"
}

#include <algorithm>
#include <vector>
#include <deque>
#include <chrono>
#include <thread>
#include <string>
#include <getopt.h>
#include <cstdio>
#include <stdint.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>

#include "aacenc_lib.h"

extern "C" {
#include "fec/fec.h"
#include "libtoolame-dab/toolame.h"
}

/* Due to memory leaks in the VLC input,
 * we don't want to restart it endlessly. */
constexpr int MAX_FAULTS_ALLOWED = 5;

using vec_u8 = std::vector<uint8_t>;

using namespace std;

static void usage(const char* name)
{
    fprintf(stderr,
    "ODR-AudioEnc %s is an audio encoder for both DAB and DAB+.\n"
    "The encoder can read from JACK, ALSA or\n"
    "a file source and encode to a ZeroMQ output for ODR-DabMux.\n"
    "It can also use libvlc and GStreamer as input.\n"
    "\n"
    "The -D option enables sound card clock drift compensation.\n"
    "A consumer sound card has a clock that is always a bit imprecise, and\n"
    "would drift off slowly. ODR-DabMux cannot handle such drift\n"
    "because it would have to throw away or insert complete encoded audio frames,\n"
    "which would create audible artifacts. This drift compensation can\n"
    "make sure that the encoding rate is correct by inserting or deleting\n"
    "audio samples. It can be used for both ALSA and VLC inputs and requires\n"
    "a system clock synchronised using NTP.\n"
    "\n"
    "When this option is enabled, you will see U and O printed in the\n"
    "console. These correspond to audio underruns and overruns caused\n"
    "by sound card clock drift. When sparse, they should not create audible\n"
    "artifacts.\n"
    "\n"
    "This encoder is able to insert PAD (DLS and MOT Slideshow)\n"
    "generated by ODR-PadEnc, and communicates using a UNIX socket.\n"
    "\nUsage:\n"
    "%s [INPUT SELECTION] [OPTION...]\n",
#if defined(GITVERSION)
    GITVERSION
#else
    PACKAGE_VERSION
#endif
    , name);
    fprintf(stderr,
    "   For the alsa input:\n"
#if HAVE_ALSA
    "     -d, --device=alsa_device             Set ALSA input device.\n"
#else
    "     The Alsa input was disabled at compile time\n"
#endif
    "   For the file input:\n"
    "     -i, --input=FILENAME                 Input filename (use -i - for stdin).\n"
    "     -f, --format={ wav, raw }            Set input file format (default: wav).\n"
    "         --fifo-silence                   Input file is fifo and encoder generates silence when fifo is empty. Ignore EOF.\n"
    "   For the JACK input:\n"
#if HAVE_JACK
    "     -j, --jack=name                      Enable JACK input, and define our name\n"
#else
    "     The JACK input was disabled at compile-time\n"
#endif
    "   For the VLC input:\n"
#if HAVE_VLC
    "     -v, --vlc-uri=uri                    Enable VLC input and use the URI given as source\n"
    "     -C, --vlc-cache=ms                   Specify VLC network cache length.\n"
    "     -V                                   Increase the VLC verbosity by one (can be given \n"
    "                                          multiple times)\n"
    "     -L OPTION                            Give an additional options to VLC (can be given\n"
    "                                          multiple times)\n"
#else
    "     The VLC input was disabled at compile-time\n"
#endif
    "   For the GStreamer input:\n"
#if HAVE_GST
    "     -G, --gst-uri=uri                    Enable GStreamer input and use the URI given as source\n"
    "         --gst-pipeline=pipeline          Specify a GStreamer pipeline that receives your source.\n"
    "                                          The last pipeline element is connected to a caps filter that specifies\n"
    "                                          the audio format and sample rate.\n"
#else
    "     The GStreamer input was disabled at compile-time\n"
#endif
    "     -w, --write-icy-text=filename        Write the ICY Text into the file, so that ODR-PadEnc can read it.\n"
    "     -W, --write-icy-text-dl-plus         When writing the ICY Text into the file, add DL Plus information.\n"
    "   Drift compensation\n"
    "     -D, --drift-comp                     Enable ALSA/VLC sound card drift compensation.\n"
    "   Encoder parameters:\n"
    "     -b, --bitrate={ 8, 16, ..., 192 }    Output bitrate in kbps. Must be a multiple of 8.\n"
    "     -c, --channels={ 1, 2 }              Nb of input channels (default: 2).\n"
    "     -r, --rate={ 24000, 32000, 48000 }   Input sample rate (default: 48000).\n"
    "     -g, --audio-gain=dB                  Apply audio gain correction in dB to source, negative values allowed.\n"
    "                                          Use this as a workaround to correct the gain for streams that are\n"
    "                                          much too loud.\n"
    "   DAB specific options\n"
    "     -a, --dab                            Encode in DAB and not in DAB+.\n"
    "         --dabmode=MODE                   Channel mode: s/d/j/m\n"
    "                                          (default: j if stereo, m if mono).\n"
    "         --dabpsy=PSY                     Psychoacoustic model 0/1/2/3\n"
    "                                          (default: 1).\n"
    "   DAB+ specific options\n"
    "     -A, --no-afterburner                 Disable AAC encoder quality increaser.\n"
    "         --aaclc                          Force the usage of AAC-LC (no SBR, no PS)\n"
    "         --sbr                            Force the usage of SBR (HE-AAC)\n"
    "         --ps                             Force the usage of SBR and PS (HE-AACv2)\n"
    "     -B, --bandwidth=VALUE                Set the AAC encoder bandwidth to VALUE [Hz].\n"
    "         --decode=FILE                    Decode the AAC back to a wav file (loopback test).\n"
    "   Output and PAD parameters:\n"
    "         --identifier=ID                  An identifier string that is sent in the ODRv EDI TAG. Max 32 characters length.\n"
    "     -o, --output=URI                     Output ZMQ uri. (e.g. 'tcp://localhost:9000')\n"
    "                                     -or- Output file uri. (e.g. 'file.dabp')\n"
    "                                     -or- a single dash '-' to denote stdout\n"
    "                                          If more than one ZMQ output is given, the socket\n"
    "                                          will be connected to all listed endpoints.\n"
    "     -e, --edi=URI                        EDI output uri, (e.g. 'tcp://localhost:7000')\n"
    "         --fec=FEC                        Set EDI output FEC\n"
    "         --edi-verbose                    Enable verbose mode for EDI output.\n"
    "     -T, --timestamp-delay=DELAY_MS       Enabled timestamps in EDI (requires TAI clock bulletin download) and\n"
    "                                          add a delay (in milliseconds) to the timestamps carried in EDI\n"
    "         --startup-check=SCRIPT_PATH      Before starting, run the given script, and only start if it returns 0.\n"
    "     -k, --secret-key=FILE                Enable ZMQ encryption with the given secret key.\n"
    "     -p, --pad=BYTES                      Enable PAD insertion and set PAD size in bytes.\n"
    "     -P, --pad-socket=IDENTIFIER          Use the given identifier to communicate with ODR-PadEnc.\n"
    "     -l, --level                          Show peak audio level indication.\n"
    "     -S, --stats=SOCKET_NAME              Connect to the specified UNIX Datagram socket and send statistics.\n"
    "                                          This allows external tools to collect audio and drift compensation stats.\n"
    "     -s, --silence=TIMEOUT                Abort encoding after TIMEOUT seconds of silence.\n"
    "         --version                        Show version and quit.\n"
    "\n"
    );

}

/*! Setup the FDK AAC encoder
 *
 * \return 0 on success
 */
static int prepare_aac_encoder(
        HANDLE_AACENCODER *encoder,
        int subchannel_index,
        int channels,
        int sample_rate,
        int afterburner,
        uint32_t bandwidth,
        int *aot)
{
    CHANNEL_MODE mode;
    switch (channels) {
        case 1: mode = MODE_1; break;
        case 2: mode = MODE_2; break;
        default:
                fprintf(stderr, "Unsupported channels number %d\n", channels);
                return 1;
    }


    if (aacEncOpen(encoder, 0x01|0x02|0x04, channels) != AACENC_OK) {
        fprintf(stderr, "Unable to open encoder\n");
        return 1;
    }

    if (*aot == AOT_NONE) {

        if(channels == 2 && subchannel_index <= 6) {
            *aot = AOT_DABPLUS_PS;
        }
        else if((channels == 1 && subchannel_index <= 8) ||
                (channels == 2 && subchannel_index <= 10)) {
            *aot = AOT_DABPLUS_SBR;
        }
        else {
            *aot = AOT_DABPLUS_AAC_LC;
        }
    }

    fprintf(stderr, "Using %d subchannels. AAC type: %s%s%s. channels=%d, sample_rate=%d\n",
            subchannel_index,
            *aot == AOT_DABPLUS_PS ? "HE-AAC v2" : "",
            *aot == AOT_DABPLUS_SBR ? "HE-AAC" : "",
            *aot == AOT_DABPLUS_AAC_LC ? "AAC-LC" : "",
            channels, sample_rate);

    if (aacEncoder_SetParam(*encoder, AACENC_AOT, *aot) != AACENC_OK) {
        fprintf(stderr, "Unable to set the AOT\n");
        return 1;
    }
    if (aacEncoder_SetParam(*encoder, AACENC_SAMPLERATE, sample_rate) != AACENC_OK) {
        fprintf(stderr, "Unable to set the sample rate\n");
        return 1;
    }
    if (aacEncoder_SetParam(*encoder, AACENC_CHANNELMODE, mode) != AACENC_OK) {
        fprintf(stderr, "Unable to set the channel mode\n");
        return 1;
    }
    if (aacEncoder_SetParam(*encoder, AACENC_CHANNELORDER, 1) != AACENC_OK) {
        fprintf(stderr, "Unable to set the wav channel order\n");
        return 1;
    }
    if (aacEncoder_SetParam(*encoder, AACENC_GRANULE_LENGTH, 960) != AACENC_OK) {
        fprintf(stderr, "Unable to set the granule length\n");
        return 1;
    }
    if (aacEncoder_SetParam(*encoder, AACENC_TRANSMUX, TT_DABPLUS) != AACENC_OK) {
        fprintf(stderr, "Unable to set the RAW transmux\n");
        return 1;
    }

    /*if (aacEncoder_SetParam(*encoder, AACENC_BITRATEMODE, AACENC_BR_MODE_SFR)
     * != AACENC_OK) {
        fprintf(stderr, "Unable to set the bitrate mode\n");
        return 1;
    }*/


    fprintf(stderr, "AAC bitrate set to: %d\n", subchannel_index*8000);
    if (aacEncoder_SetParam(*encoder, AACENC_BITRATE, subchannel_index*8000) != AACENC_OK) {
        fprintf(stderr, "Unable to set the bitrate\n");
        return 1;
    }
    if (aacEncoder_SetParam(*encoder, AACENC_AFTERBURNER, afterburner) != AACENC_OK) {
        fprintf(stderr, "Unable to set the afterburner mode\n");
        return 1;
    }
    if (!afterburner) {
        fprintf(stderr, "Warning: Afterburned disabled!\n");
    }

    if (bandwidth > 0) {
        fprintf(stderr, "Setting bandwidth is %d\n", bandwidth);
        if (aacEncoder_SetParam(*encoder, AACENC_BANDWIDTH, bandwidth) != AACENC_OK) {
            fprintf(stderr, "Unable to set bandwidth mode\n");
            return 1;
        }
    }
    if (aacEncEncode(*encoder, nullptr, nullptr, nullptr, nullptr) != AACENC_OK) {
        fprintf(stderr, "Unable to initialize the encoder\n");
        return 1;
    }

    const uint32_t bw = aacEncoder_GetParam(*encoder, AACENC_BANDWIDTH);
    fprintf(stderr, "Bandwidth is %d\n", bw);

    return 0;
}

chrono::steady_clock::time_point timepoint_last_compensation;

/*! Do drift compensation by distributing the missing samples over
 *  the whole input buffer instead of having a bunch of missing samples
 *  at the end only.
 *
 *  This expands (in time) the received samples over the whole duration
 *  of the buffer.
 */
static void expand_missing_samples(vec_u8& buf, int channels, size_t valid_bytes)
{
    const size_t bytes_per_sample = BYTES_PER_SAMPLE * channels;
    assert(buf.size() % bytes_per_sample == 0);
    assert(buf.size() > valid_bytes);
    const size_t valid_samples = valid_bytes / bytes_per_sample;
    const size_t missing_samples =
        (buf.size() / bytes_per_sample) - valid_samples;

    // We only fix up to 10% missing samples
    if (missing_samples * bytes_per_sample > buf.size() / 10) {
        for (size_t i = valid_samples * bytes_per_sample; i < buf.size(); i++) {
            buf[i] = 0;
        }
    }
    else {
        const vec_u8 source_buf(buf);
        size_t source_ix = 0;

        for (size_t i = 0; i < buf.size() / bytes_per_sample; i++) {
            for (size_t j = 0; j < bytes_per_sample; j++) {
                buf.at(bytes_per_sample*i + j) = source_buf.at(source_ix + j);
            }

            // Do not advance the source index if the sample index is
            // at the spots where we want to duplicate the source sample
            if (not (i > 0 and (i % (valid_samples / missing_samples) == 0))) {
                source_ix += bytes_per_sample;
            }
        }
    }
}

/*! Wait the proper amount of time to throttle down to nominal encoding
 * rate, if drift compensation is enabled.
 */
static void drift_compensation_delay(int sample_rate, int channels, size_t bytes)
{
    const size_t bytes_per_second = sample_rate * BYTES_PER_SAMPLE * channels;

    size_t bytes_compensate = bytes;
    const auto wait_time = std::chrono::milliseconds(1000ul * bytes_compensate / bytes_per_second);
    assert(1000ul * bytes_compensate % bytes_per_second == 0);

    const auto curTime = std::chrono::steady_clock::now();

    const auto diff = curTime - timepoint_last_compensation;

    if (diff < wait_time) {
        auto waiting = wait_time - diff;
        std::this_thread::sleep_for(waiting);
    }

    timepoint_last_compensation += wait_time;
}



#define no_argument 0
#define required_argument 1
#define optional_argument 2

#define STATUS_PAD_INSERTED 0x1
#define STATUS_OVERRUN 0x2
#define STATUS_UNDERRUN 0x4

struct AudioEnc {
public:
    int sample_rate=48000;
    int channels=2;
    double gain_dB = 0.0;

    string icytext_file;
    bool icytext_dlplus = false;
    ICY_TEXT_t previous_text;

    // For the ALSA input
    string alsa_device;

    // For the file input
    string infile;
    bool continue_after_eof = false;
    int raw_input = 0;

    // For the VLC input
    string vlc_uri;
    string vlc_cache;
    vector<string> vlc_additional_opts;
    unsigned verbosity = 0;

    // For the GST input
    string gst_uri;
    string gst_pipeline;

    string jack_name;

    bool drift_compensation = false;

    encoder_selection_t selected_encoder = encoder_selection_t::fdk_dabplus;
    bool afterburner = true;
    uint32_t bandwidth = 0;
    int bitrate = 0; // 0 means default bitrate

    int dab_psy_model = 1;

    bool restart_on_fault = false;
    int fault_counter = 0;

    std::deque<uint8_t> toolame_buffer;

    shared_ptr<Output::File> file_output;
    shared_ptr<Output::ZMQ> zmq_output;
    Output::EDI edi_output;
    string identifier;

    bool tist_enabled = false;
    uint32_t tist_delay_ms = 0;

    vector<string> output_uris;
    vector<string> edi_output_uris;

    void *rs_handler = nullptr;
    AACENC_InfoStruct info = { 0 };
    int aot = AOT_NONE;

    string decode_wavfilename;

    string dab_channel_mode;

    /* On silence, die after the silence_timeout expires */
    bool die_on_silence = false;
    int silence_timeout = 0;
    int measured_silence_ms = 0;

    /* For MOT Slideshow and DLS insertion */
    string pad_ident = "";
    PadInterface pad_intf;
    int padlen = 6;

    /* Encoder status, see the above STATUS macros */
    int status = 0;

    /* Whether to show the 'sox'-like measurement */
    int show_level = 0;

    /* If not empty, send stats over UNIX DGRAM socket */
    string send_stats_to = "";

    /* Data for ZMQ CURVE authentication */
    char* keyfile = nullptr;
    char secretkey[CURVE_KEYLEN+1];

    SampleQueue<uint8_t> queue;

    HANDLE_AACENCODER encoder = nullptr;
    unique_ptr<AACDecoder> decoder;
    unique_ptr<StatsPublisher> stats_publisher;

    AudioEnc() : queue(BYTES_PER_SAMPLE) { }
    AudioEnc(const AudioEnc&) = delete;
    AudioEnc& operator=(const AudioEnc&) = delete;
    ~AudioEnc();

    int run();
    bool send_frame(const uint8_t *buf, size_t len, int16_t peak_left, int16_t peak_right);
    shared_ptr<InputInterface> initialise_input();
};

int AudioEnc::run()
{
    int num_inputs = 0;
#if HAVE_ALSA
    if (not alsa_device.empty()) num_inputs++;
#endif
    if (not infile.empty()) num_inputs++;
#if HAVE_JACK
    if (not jack_name.empty()) num_inputs++;
#endif
#if HAVE_VLC
    if (not vlc_uri.empty()) num_inputs++;
#endif
#if HAVE_GST
    if (not gst_uri.empty()) num_inputs++;
    if (not gst_pipeline.empty()) num_inputs++;
#endif

    if (num_inputs == 0) {
        fprintf(stderr, "No input defined!\n");
        return 1;
    }
    else if (num_inputs > 1) {
        fprintf(stderr, "You must define only one possible input, not several!\n");
        return 1;
    }

    if (selected_encoder == encoder_selection_t::fdk_dabplus) {
        if (bitrate == 0) {
            bitrate = 64;
        }

        int subchannel_index = bitrate / 8;

        if (subchannel_index < 1 || subchannel_index > 24) {
            fprintf(stderr, "Bad subchannel index: %d, must be between 1 and 24. Try other bitrate.\n",
                    subchannel_index);
            return 1;
        }

        if ( ! (sample_rate == 32000 || sample_rate == 48000)) {
            fprintf(stderr, "Invalid sample rate. Possible values are: 32000, 48000.\n");
            return 1;
        }
    }
    else if (selected_encoder == encoder_selection_t::toolame_dab) {
        if (bitrate == 0) {
            bitrate = 192;
        }

        if ( ! (sample_rate == 24000 || sample_rate == 48000)) {
            fprintf(stderr, "Invalid sample rate. Possible values are: 24000, 48000.\n");
            return 1;
        }
    }

    if (padlen < 0 or padlen > 255) {
        fprintf(stderr, "Invalid PAD length specified\n");
        return 1;
    }

    if (output_uris.empty() and edi_output_uris.empty()) {
        fprintf(stderr, "No output defined\n");
        return 1;
    }

    for (const auto& uri : output_uris) {
        if (uri == "-") {
            if (file_output) {
                fprintf(stderr, "You can't write to more than one file!\n");
                return 1;
            }
            file_output = make_shared<Output::File>(stdout);
        }
        else if ((uri.compare(0, 6, "tcp://") == 0) ||
                (uri.compare(0, 6, "pgm://") == 0) ||
                (uri.compare(0, 7, "epgm://") == 0) ||
                (uri.compare(0, 6, "ipc://") == 0)) {

            if (not zmq_output) {
                zmq_output = make_shared<Output::ZMQ>();
            }

            zmq_output->connect(uri.c_str(), keyfile);
        }
        else { // We assume it's a file name
            if (file_output) {
                fprintf(stderr, "You can't write to more than one file!\n");
                return 1;
            }
            file_output = make_shared<Output::File>(uri.c_str());
        }
    }

    for (const auto& uri : edi_output_uris) {
        if (uri.compare(0, 6, "tcp://") == 0 or
            uri.compare(0, 6, "udp://") == 0) {
            auto host_port_sep_ix = uri.find(':', 6);
            if (host_port_sep_ix != string::npos) {
                auto host = uri.substr(6, host_port_sep_ix - 6);
                auto port = std::stoi(uri.substr(host_port_sep_ix + 1));

                auto proto = uri.substr(0, 3);
                if (proto == "tcp") {
                    edi_output.add_tcp_destination(host, port);
                }
                else if (proto == "udp") {
                    edi_output.add_udp_destination(host, port);
                }
                else {
                    throw logic_error("unhandled proto");
                }
            }
            else {
                fprintf(stderr, "Invalid EDI URL host!\n");
            }
        }
        else {
            fprintf(stderr, "Invalid EDI protocol!\n");
        }
    }

    if (not edi_output_uris.empty()) {
        edi_output.set_tist(tist_enabled, tist_delay_ms);

        stringstream ss;
        ss << PACKAGE_NAME << " " <<
#if defined(GITVERSION)
            GITVERSION <<
#else
            PACKAGE_VERSION <<
#endif
            " " << identifier;
        edi_output.set_odr_version_tag(ss.str());
    }

    if (pad_ident.empty()) {
        // Override both default value and user-configured value if no ident given
        padlen = 0;
    }

    if (padlen != 0 and not pad_ident.empty()) {
        pad_intf.open(pad_ident);
        fprintf(stderr, "PAD socket opened\n");
    }
    else {
        fprintf(stderr, "PAD disabled because neither PAD length nor PAD identifier given\n");
    }

    vec_u8 input_buf;

    if (selected_encoder == encoder_selection_t::fdk_dabplus) {
        int subchannel_index = bitrate / 8;
        if (prepare_aac_encoder(&encoder, subchannel_index, channels,
                    sample_rate, afterburner, bandwidth, &aot) != 0) {
            fprintf(stderr, "Encoder preparation failed\n");
            return 1;
        }

        if (aacEncInfo(encoder, &info) != AACENC_OK) {
            fprintf(stderr, "Unable to get the encoder info\n");
            return 1;
        }

        // Each DAB+ frame will need input_size audio bytes
        const int input_size = channels * BYTES_PER_SAMPLE * info.frameLength;
        fprintf(stderr, "DAB+ Encoding: framelen=%d (%dB)\n",
                info.frameLength,
                input_size);

        input_buf.resize(input_size);

        if (not decode_wavfilename.empty()) {
            decoder.reset(new AACDecoder(decode_wavfilename.c_str()));
        }
    }
    else if (selected_encoder == encoder_selection_t::toolame_dab) {
        int err = toolame_init();

        if (err == 0) {
            err = toolame_set_samplerate(sample_rate);
        }

        if (err == 0) {
            err = toolame_set_psy_model(dab_psy_model);
        }

        if (dab_channel_mode.empty()) {
            if (channels == 2) {
                dab_channel_mode = 'j'; // Default to joint-stereo
            }
            else if (channels == 1) {
                dab_channel_mode = 'm'; // Default to mono
            }
            else {
                fprintf(stderr, "Unsupported channels number %d\n",
                        channels);
                return 1;
            }
        }

        if (err == 0) {
            err = toolame_set_channel_mode(dab_channel_mode.c_str()[0]);
        }

        // setting the ScF-CRC len here depends on set sample rate/channel mode
        if (err == 0) {
            err = toolame_set_bitrate(bitrate);
        }

        if (err == 0) {
            err = toolame_set_pad(padlen);
        }

        if (err) {
            fprintf(stderr, "libtoolame-dab init failed: %d\n", err);
            return err;
        }

        input_buf.resize(channels * 1152 * BYTES_PER_SAMPLE);

        if (not decode_wavfilename.empty()) {
            fprintf(stderr, "--decode not supported for DAB\n");
            return 1;
        }
    }

    if (not send_stats_to.empty()) {
        StatsPublisher *s = nullptr;
        try {
            s = new StatsPublisher(send_stats_to);
            stats_publisher.reset(s);
        }
        catch (const runtime_error& e) {
            fprintf(stderr, "Failed to initialise Stats Publisher: %s", e.what());
            if (s != nullptr) {
                delete s;
            }
            return 1;
        }
    }

    /* We assume that we need to call the encoder
     * enc_calls_per_output before it gives us one encoded audio
     * frame. This information is used when the alsa drift compensation
     * is active. This is only valid for FDK-AAC.
     */
    const int enc_calls_per_output = (aot == AOT_DABPLUS_AAC_LC) ?
        sample_rate / 8000 :
        sample_rate / 16000;

    int max_size = 32*input_buf.size() + NUM_SAMPLES_PER_CALL;

    /*! The SampleQueue \c queue is given to the inputs, so that they
     * can fill it.
     */
    queue.configure(max_size, not drift_compensation, channels);

    /* symsize=8, gfpoly=0x11d, fcr=0, prim=1, nroots=10, pad=135 */
    rs_handler = init_rs_char(8, 0x11d, 0, 1, 10, 135);
    if (rs_handler == nullptr) {
        perror("init_rs_char failed");
        return 1;
    }

    shared_ptr<InputInterface> input;
    try {
        input = initialise_input();
    }
    catch (const runtime_error& e) {
        fprintf(stderr, "Initialising input triggered exception: %s\n", e.what());
        return 1;
    }

    if (zmq_output) {
        zmq_output->set_encoder_type(selected_encoder, bitrate);
    }

    int outbuf_size = 0;
    vec_u8 outbuf;

    switch (selected_encoder) {
        case encoder_selection_t::fdk_dabplus:
            outbuf_size = bitrate/8*120;
            outbuf.resize(24*120);
            break;
        case encoder_selection_t::toolame_dab:
            outbuf_size = 4092;
            outbuf.resize(outbuf_size);
            fprintf(stderr, "Setting outbuf size to %zu\n", outbuf.size());
            break;
    }

    vector<uint8_t> pad_buf(padlen + 1);

    if (restart_on_fault) {
        fprintf(stderr, "Autorestart has been deprecated and will be removed in the future!\n");
        this_thread::sleep_for(chrono::seconds(2));
    }

    fprintf(stderr, "Starting encoding\n");

    int retval = 0;
    int send_error_count = 0;
    timepoint_last_compensation = chrono::steady_clock::now();
    auto timepoint_last_received_sample = chrono::steady_clock::now();

    int calls = 0; // for checking
    ssize_t read_bytes = 0;
    do {
        // --------------- Read data from the PAD socket
        int calculated_padlen = 0;

        if (padlen != 0) {
            vector<uint8_t> pad_data = pad_intf.request(padlen);

            if (pad_data.empty()) {
                /* no PAD available */
            }
            else if (pad_data.size() == pad_buf.size()) {
                calculated_padlen = pad_data[padlen];

                if (calculated_padlen < 2) {
                    throw runtime_error("Invalid X-PAD length " + to_string(calculated_padlen));
                }

                /* AAC: skip PAD if only zero F-PAD (saves four bytes)
                 * See §5.4.3 in ETSI TS 102 563
                 */
                if (    selected_encoder == encoder_selection_t::fdk_dabplus &&
                        calculated_padlen == 2 &&
                        pad_data[padlen - 2] == 0x00 &&
                        pad_data[padlen - 1] == 0x00 ) {
                    calculated_padlen = 0;
                }

                copy(pad_data.begin(), pad_data.end(), pad_buf.begin());
            }
            else {
                fprintf(stderr, "Incorrect PAD length received: %zu expected %d\n", pad_data.size(), padlen + 1);
                break;
            }
        }

        if (calculated_padlen > 0) {
            status |= STATUS_PAD_INSERTED;
        }


        // -------------- Read Data
        memset(outbuf.data(), 0x00, outbuf_size);
        memset(input_buf.data(), 0x00, input_buf.size());

        /*! \section DataInput
         * We read data input either in a blocking way (file input, VLC or ALSA
         * without drift compensation) or in a non-blocking way (VLC or ALSA
         * with drift compensation, JACK).
         *
         * All inputs write samples into the queue, and either use \c pop() or
         * \c pop_wait() depending on if it's blocking or not
         *
         * In non-blocking, the \c queue makes the data available without delay, and the
         * \c drift_compensation_delay() function handles rate throttling.
         */

        if (input->fault_detected()) {
            fprintf(stderr, "Detected fault in input!\n");

            if (restart_on_fault) {
                fault_counter++;

                if (fault_counter >= MAX_FAULTS_ALLOWED) {
                    fprintf(stderr, "Maximum number of input faults reached, aborting");
                    retval = 5;
                    break;
                }

                try {
                    input = initialise_input();
                }
                catch (const runtime_error& e) {
                    fprintf(stderr, "Initialising input triggered exception: %s\n", e.what());
                    retval = 5;
                    break;
                }

                continue;
            }
            else {
                retval = 5;
                break;
            }
        }

        if (not input->read_source(input_buf.size())) {
            fprintf(stderr, "End of input reached\n");
            retval = 0;
            break;
        }

        if (drift_compensation) {
            size_t overruns = 0;
            size_t bytes_from_queue = queue.pop(input_buf.data(), input_buf.size(), &overruns); // returns bytes
            if (bytes_from_queue != input_buf.size()) {
                expand_missing_samples(input_buf, channels, bytes_from_queue);
            }
            read_bytes = input_buf.size();
            drift_compensation_delay(sample_rate, channels, read_bytes);

            if (bytes_from_queue != input_buf.size()) {
                status |= STATUS_UNDERRUN;
                if (stats_publisher) {
                    stats_publisher->notify_underrun();
                }

                const auto now = chrono::steady_clock::now();
                const auto elapsed = chrono::duration_cast<chrono::seconds>(
                        now - timepoint_last_received_sample);
                if (elapsed.count() > 60) {
                    fprintf(stderr, "Underruns for 60s, aborting!\n");
                    return 1;
                }
            }
            else {
                timepoint_last_received_sample = chrono::steady_clock::now();
            }

            if (overruns) {
                status |= STATUS_OVERRUN;
                if (stats_publisher) {
                    stats_publisher->notify_overrun();
                }
            }
        }
        else {
            const int timeout_ms = 10000;
            read_bytes = input_buf.size();

            size_t overruns = 0;

            /*! pop_wait() must return after a timeout, otherwise the silence detector cannot do
             * its job. */
            ssize_t bytes_from_queue = queue.pop_wait(input_buf.data(), read_bytes, timeout_ms, &overruns); // returns bytes

            if (overruns) {
                throw logic_error("Queue overrun in non-drift compensation!");
            }

            if (bytes_from_queue < read_bytes) {
                // queue timeout occurred
                fprintf(stderr, "Detected fault in input! No data in time.\n");

                if (restart_on_fault) {
                    fault_counter++;

                    if (fault_counter >= MAX_FAULTS_ALLOWED) {
                        fprintf(stderr, "Maximum number of input faults reached, aborting");
                        retval = 5;
                        break;
                    }

                    try {
                        input = initialise_input();
                    }
                    catch (const runtime_error& e) {
                        fprintf(stderr, "Initialising input triggered exception: %s\n", e.what());
                        return 1;
                    }

                    continue;
                }
                else {
                    retval = 5;
                    break;
                }
            }
        }

        /*! \section MetadataFromSource
         * The VLC input is the only input that can also give us metadata, which
         * we can hand over to ODR-PadEnc.
         */
        if (not icytext_file.empty()) {
            ICY_TEXT_t text;

            if (false) {}
#if HAVE_VLC
            // Using std::dynamic_pointer_cast would be safer, but is C++17
            else if (not vlc_uri.empty()) {
                VLCInput *vlc_input = (VLCInput*)(input.get());
                text = vlc_input->get_icy_text();
            }
#endif
#if HAVE_GST
            else if ((not gst_uri.empty()) or (not gst_pipeline.empty())) {
                GSTInput *gst_input = (GSTInput*)(input.get());
                text = gst_input->get_icy_text();
            }
#endif

            if (previous_text != text) {
                bool success = write_icy_to_file(text, icytext_file, icytext_dlplus);

                if (not success) {
                    fprintf(stderr, "Failed to write ICY Text\n");
                }
            }

            previous_text = text;
        }

        /*! \section AudioLevel
         * Audio level measurement is always done assuming we have two
         * channels, and is formally wrong in mono, but still gives
         * numbers one can use.
         *
         * At the same time, we apply gain correction.
         *
         * \todo fix level measurement in mono
         */
        int16_t peak_left  = 0;
        int16_t peak_right = 0;

        const double linear_gain_correction = pow(10.0, gain_dB / 20.0);

        for (int i = 0; i < read_bytes; i+=4) {
            int16_t l = input_buf[i] | (input_buf[i+1] << 8);
            int16_t r = input_buf[i+2] | (input_buf[i+3] << 8);

            if (linear_gain_correction != 1.0) {
                l *= linear_gain_correction;
                r *= linear_gain_correction;

                input_buf[i] = l & 0x00FF;
                input_buf[i+1] = (l & 0xFF00) >> 8;
                input_buf[i+2] = r & 0x00FF;
                input_buf[i+3] = (r & 0xFF00) >> 8;
            }

            peak_left  = std::max(peak_left,  l);
            peak_right = std::max(peak_right, r);
        }

        if (stats_publisher) {
            stats_publisher->update_audio_levels(peak_left, peak_right);
        }

        /*! \section SilenceDetection
         * Silence detection looks at the audio level and is
         * only useful if the connection dropped, or if no data is available. It is not
         * useful if the source is nearly silent (some noise present), because the
         * threshold is 0, and not configurable. The rationale is that we want to
         * guard against connection issues, not source level issues.
         */
        if (die_on_silence && std::max(peak_left, peak_right) == 0) {
            const unsigned int frame_time_msec = 1000ul *
                read_bytes / (BYTES_PER_SAMPLE * channels * sample_rate);

            measured_silence_ms += frame_time_msec;

            if (measured_silence_ms > 1000*silence_timeout) {
                fprintf(stderr, "Silence detected for %d seconds, aborting.\n",
                        silence_timeout);
                retval = 2;
                break;
            }
        }
        else {
            measured_silence_ms = 0;
        }

        int numOutBytes = 0;
        if (read_bytes and
                selected_encoder == encoder_selection_t::fdk_dabplus) {
            AACENC_BufDesc in_buf = { 0 }, out_buf = { 0 };
            AACENC_InArgs in_args = { 0 };
            AACENC_OutArgs out_args = { 0 };
            // -------------- AAC Encoding
            //
            int in_identifier[] = {IN_AUDIO_DATA, IN_ANCILLRY_DATA};
            int out_identifier = OUT_BITSTREAM_DATA;

            void *in_ptr[2], *out_ptr;
            int in_size[2], in_elem_size[2];
            int out_size, out_elem_size;

            in_ptr[0] = input_buf.data();
            in_ptr[1] = pad_buf.data() + (padlen - calculated_padlen); // offset due to unused PAD bytes
            in_size[0] = read_bytes;
            in_size[1] = calculated_padlen;
            in_elem_size[0] = BYTES_PER_SAMPLE;
            in_elem_size[1] = sizeof(uint8_t);
            in_args.numInSamples = input_buf.size()/BYTES_PER_SAMPLE;
            in_args.numAncBytes = calculated_padlen;

            in_buf.numBufs = calculated_padlen ? 2 : 1;    // Samples + Data / Samples
            in_buf.bufs = (void**)&in_ptr;
            in_buf.bufferIdentifiers = in_identifier;
            in_buf.bufSizes = in_size;
            in_buf.bufElSizes = in_elem_size;

            out_ptr = outbuf.data();
            out_size = outbuf.size();
            out_elem_size = 1;
            out_buf.numBufs = 1;
            out_buf.bufs = &out_ptr;
            out_buf.bufferIdentifiers = &out_identifier;
            out_buf.bufSizes = &out_size;
            out_buf.bufElSizes = &out_elem_size;

            AACENC_ERROR err;
            if ((err = aacEncEncode(encoder, &in_buf, &out_buf, &in_args, &out_args))
                    != AACENC_OK) {
                if (err == AACENC_ENCODE_EOF) {
                    fprintf(stderr, "encoder error: EOF reached\n");
                    break;
                }
                fprintf(stderr, "Encoding failed (%d)\n", err);
                retval = 3;
                break;
            }
            calls++;

            numOutBytes = out_args.numOutBytes;
        }
        else if (selected_encoder == encoder_selection_t::toolame_dab) {
            /*! \note toolame expects the audio to be in another shape as
             * we have in input_buf, and we need to convert first
             */
            short input_buffers[2][1152];

            if (channels == 1) {
                memcpy(input_buffers[0], input_buf.data(), 1152 * BYTES_PER_SAMPLE);
            }
            else if (channels == 2) {
                for (int i = 0; i < 1152; i++) {
                    int16_t l = input_buf[4*i]   | (input_buf[4*i+1] << 8);
                    int16_t r = input_buf[4*i+2] | (input_buf[4*i+3] << 8);

                    input_buffers[0][i] = l;
                    input_buffers[1][i] = r;
                }
            }
            else {
                fprintf(stderr, "INTERNAL ERROR! invalid number of channels\n");
            }

            if (read_bytes) {
                numOutBytes = toolame_encode_frame(input_buffers, pad_buf.data(), calculated_padlen, outbuf.data(), outbuf.size());
            }
            else {
                numOutBytes = toolame_finish(outbuf.data(), outbuf.size());
            }
        }

        if (numOutBytes != 0 and decoder) {
            try {
                decoder->decode_frame(outbuf.data(), numOutBytes);
            }
            catch (runtime_error &e) {
                fprintf(stderr, "Decoding failed with: %s\n", e.what());
                return 1;
            }
        }

        /* Check if the encoder has generated output data.
         * DAB+ requires RS encoding, which is not done in ODR-DabMux and not necessary
         * for DAB.
         */
        if (numOutBytes != 0 and
            selected_encoder == encoder_selection_t::fdk_dabplus) {

            // Our timing code depends on this
            if (calls != enc_calls_per_output) {
                fprintf(stderr, "INTERNAL ERROR! calls=%d, expected %d\n",
                        calls, enc_calls_per_output);
            }
            calls = 0;

            int row, col;
            unsigned char buf_to_rs_enc[110];
            unsigned char rs_enc[10];
            const int subchannel_index = bitrate / 8;
            for(row=0; row < subchannel_index; row++) {
                for(col=0;col < 110; col++) {
                    buf_to_rs_enc[col] = outbuf[subchannel_index * col + row];
                }

                encode_rs_char(rs_handler, buf_to_rs_enc, rs_enc);

                for(col=110; col<120; col++) {
                    outbuf.at(subchannel_index * col + row) = rs_enc[col-110];
                }
            }

            numOutBytes = outbuf_size;
        }

        if (numOutBytes > 0 and selected_encoder == encoder_selection_t::toolame_dab) {
            toolame_buffer.insert(toolame_buffer.end(), outbuf.begin(), outbuf.begin() + numOutBytes);

            // ODR-DabMux expects frames of length 3*bitrate
            const size_t frame_len = 3 * bitrate;
            while (toolame_buffer.size() > frame_len) {
                vec_u8 frame(frame_len);
                // this is probably not very efficient
                std::copy(toolame_buffer.begin(), toolame_buffer.begin() + frame_len, frame.begin());
                toolame_buffer.erase(toolame_buffer.begin(), toolame_buffer.begin() + frame_len);

                bool success = send_frame(frame.data(), frame.size(), peak_left, peak_right);
                if (not success) {
                    fprintf(stderr, "Send error !\n");
                    send_error_count ++;
                }
            }
        }
        else if (numOutBytes > 0 and selected_encoder == encoder_selection_t::fdk_dabplus) {
            bool success = send_frame(outbuf.data(), numOutBytes, peak_left, peak_right);
            if (not success) {
                fprintf(stderr, "Send error !\n");
                send_error_count ++;
            }
        }

        if (send_error_count > 10) {
            fprintf(stderr, "Send failed ten times, aborting!\n");
            retval = 4;
            break;
        }

        if (numOutBytes != 0) {
            if (show_level) {
                if (channels == 1) {
                    fprintf(stderr, "\rIn: [%-6s] %1s %1s %1s",
                            level(1, std::max(peak_right, peak_left)),
                            status & STATUS_PAD_INSERTED ? "P" : " ",
                            status & STATUS_UNDERRUN ? "U" : " ",
                            status & STATUS_OVERRUN ? "O" : " ");
                }
                else if (channels == 2) {
                    fprintf(stderr, "\rIn: [%6s|%-6s] %1s %1s %1s",
                            level(0, peak_left),
                            level(1, peak_right),
                            status & STATUS_PAD_INSERTED ? "P" : " ",
                            status & STATUS_UNDERRUN ? "U" : " ",
                            status & STATUS_OVERRUN ? "O" : " ");
                }
            }
            else {
                if (status & STATUS_OVERRUN) {
                    fprintf(stderr, "O");
                }

                if (status & STATUS_UNDERRUN) {
                    fprintf(stderr, "U");
                }
            }

            if (stats_publisher) {
                stats_publisher->send_stats();
            }

            status = 0;
        }

        fflush(stdout);
    } while (read_bytes > 0);

    fprintf(stderr, "\n");
    return retval;
}

bool AudioEnc::send_frame(const uint8_t *buf, size_t len, int16_t peak_left, int16_t peak_right)
{
    // The file output is mutually exclusive to the other outputs
    if (file_output) {
        file_output->update_audio_levels(peak_left, peak_right);
        return file_output->write_frame(buf, len);
    }

    bool success = true;
    if (zmq_output) {
        zmq_output->update_audio_levels(peak_left, peak_right);
        success &= zmq_output->write_frame(buf, len);
    }

    if (edi_output.enabled()) {
        edi_output.update_audio_levels(peak_left, peak_right);
        switch (selected_encoder) {
            case encoder_selection_t::fdk_dabplus:
                {
                    // STI/EDI specifies that one AF packet must contain 24ms worth of data,
                    // therefore we must split the superframe into five parts
                    if (len % 5 != 0) {
                        throw logic_error("Superframe size not multiple of 5");
                    }

                    const size_t blocksize = len/5;
                    for (size_t i = 0; i < 5; i++) {
                        success &= edi_output.write_frame(buf + i * blocksize, blocksize);
                        if (not success) {
                            break;
                        }
                    }
                }
                break;
            case encoder_selection_t::toolame_dab:
                success &= edi_output.write_frame(buf, len);
                break;
        }
    }
    return success;
}

AudioEnc::~AudioEnc()
{
    file_output.reset();
    zmq_output.reset();

    if (rs_handler != nullptr) {
        free_rs_char(rs_handler);
    }

    if (encoder != nullptr and selected_encoder == encoder_selection_t::fdk_dabplus) {
        aacEncClose(&encoder);
    }
}

shared_ptr<InputInterface> AudioEnc::initialise_input()
{
    shared_ptr<InputInterface> input;

    if (not infile.empty()) {
        input = make_shared<FileInput>(infile, raw_input, sample_rate, continue_after_eof, queue);
    }
#if HAVE_JACK
    else if (not jack_name.empty()) {
        input = make_shared<JackInput>(jack_name, channels, sample_rate, queue);
    }
#endif
#if HAVE_VLC
    else if (not vlc_uri.empty()) {
        input = make_shared<VLCInput>(vlc_uri, sample_rate, channels, verbosity,
                vlc_cache, vlc_additional_opts, queue);
    }
#endif
#if HAVE_GST
    else if ((not gst_uri.empty()) or (not gst_pipeline.empty())) {
        input = make_shared<GSTInput>(gst_uri, gst_pipeline, sample_rate, channels, queue);
    }
#endif
#if HAVE_ALSA
    else if (drift_compensation) {
        input = make_shared<AlsaInputThreaded>(alsa_device, channels, sample_rate, queue);
    }
    else {
        input = make_shared<AlsaInputDirect>(alsa_device, channels, sample_rate, queue);
    }
#endif

    if (not input) {
        throw logic_error("Initialising input incomplete!");
    }

    input->prepare();

    return input;
}

int main(int argc, char *argv[])
{
    const struct option longopts[] = {
        {"bitrate",                required_argument,  0, 'b'},
        {"bandwidth",              required_argument,  0, 'B'},
        {"audio-gain",             required_argument,  0, 'g'},
        {"vlc-gain",               required_argument,  0, 10 }, // backward-compatibility to v3
        {"channels",               required_argument,  0, 'c'},
        {"dabmode",                required_argument,  0,  4 },
        {"dabpsy",                 required_argument,  0,  5 },
        {"device",                 required_argument,  0, 'd'},
        {"edi",                    required_argument,  0, 'e'},
        {"fec",                    required_argument,  0,  8 },
        {"timestamp-delay",        required_argument,  0, 'T'},
        {"decode",                 required_argument,  0,  6 },
        {"format",                 required_argument,  0, 'f'},
        {"gst-uri",                required_argument,  0, 'G'},
        {"gst-pipeline",           required_argument,  0, 11 },
        {"identifier",             required_argument,  0,  7 },
        {"input",                  required_argument,  0, 'i'},
        {"jack",                   required_argument,  0, 'j'},
        {"output",                 required_argument,  0, 'o'},
        {"pad",                    required_argument,  0, 'p'},
        {"pad-socket",             required_argument,  0, 'P'},
        {"rate",                   required_argument,  0, 'r'},
        {"secret-key",             required_argument,  0, 'k'},
        {"silence",                required_argument,  0, 's'},
        {"startup-check",          required_argument,  0,  9 },
        {"stats",                  required_argument,  0, 'S'},
        {"vlc-cache",              required_argument,  0, 'C'},
        {"vlc-uri",                required_argument,  0, 'v'},
        {"vlc-opt",                required_argument,  0, 'L'},
        {"write-icy-text",         required_argument,  0, 'w'},
        {"write-icy-text-dl-plus", no_argument,        0, 'W'},
        {"aaclc",                  no_argument,        0,  0 },
        {"dab",                    no_argument,        0, 'a'},
        {"drift-comp",             no_argument,        0, 'D'},
        {"edi-verbose",            no_argument,        0, 12 },
        {"fifo-silence",           no_argument,        0,  3 },
        {"help",                   no_argument,        0, 'h'},
        {"level",                  no_argument,        0, 'l'},
        {"no-afterburner",         no_argument,        0, 'A'},
        {"ps",                     no_argument,        0,  2 },
        {"restart",                no_argument,        0, 'R'},
        {"sbr",                    no_argument,        0,  1 },
        {"verbosity",              no_argument,        0, 'V'},
        {0, 0, 0, 0},
    };

    if (argc == 2 and strcmp(argv[1], "--version") == 0) {
        fprintf(stdout, "%s\n",
#if defined(GITVERSION)
                GITVERSION
#else
                PACKAGE_VERSION
#endif
               );
        return 0;
    }

    fprintf(stderr,
            "Welcome to %s %s, compiled at %s, %s",
            PACKAGE_NAME,
#if defined(GITVERSION)
            GITVERSION,
#else
            PACKAGE_VERSION,
#endif
            __DATE__, __TIME__);
    fprintf(stderr, "\n");
    fprintf(stderr, "  http://opendigitalradio.org\n\n");


    if (argc < 2) {
        usage(argv[0]);
        return 1;
    }

    AudioEnc audio_enc;

    std::string startupcheck;

    int ch=0;
    int index;
    while(ch != -1) {
        ch = getopt_long(argc, argv, "aAhDlRVb:B:c:e:f:G:i:j:k:L:o:r:d:p:P:s:S:T:v:w:Wg:C:", longopts, &index);
        switch (ch) {
        case 0: // AAC-LC
            audio_enc.aot = AOT_DABPLUS_AAC_LC;
            break;
        case 1: // SBR
            audio_enc.aot = AOT_DABPLUS_SBR;
            break;
        case 2: // PS
            audio_enc.aot = AOT_DABPLUS_PS;
            break;
        case 3: // FIFO Silence
            audio_enc.continue_after_eof = true;
            // Enable drift compensation, otherwise we would block instead of inserting silence.
            audio_enc.drift_compensation = true;
            break;
        case 4: // DAB channel mode
            audio_enc.dab_channel_mode = optarg;
            if (not(    audio_enc.dab_channel_mode == "s" or
                        audio_enc.dab_channel_mode == "d" or
                        audio_enc.dab_channel_mode == "j" or
                        audio_enc.dab_channel_mode == "m")) {
                fprintf(stderr, "Invalid DAB channel mode\n");
                usage(argv[0]);
                return 1;
            }
            break;
        case 5: // DAB psy model
            audio_enc.dab_psy_model = std::stoi(optarg);
            break;
        case 6: // Enable loopback decoder for AAC
            audio_enc.decode_wavfilename = optarg;
            break;
        case 7: // Identifier for in-band version information
            audio_enc.identifier = optarg;
            /* The 32 character length restriction is arbitrary, but guarantees
             * that the EDI packet will not grow too large */
            if (audio_enc.identifier.size() > 32) {
                fprintf(stderr, "Output Identifier too long!\n");
                usage(argv[0]);
                return 1;
            }
            break;
        case 8: // EDI output FEC
            audio_enc.edi_output.set_fec(std::stoi(optarg));
            break;
        case 12: // --edi-verbose
            audio_enc.edi_output.set_verbose(true);
            break;
        case 9: // --startup-check
            startupcheck = optarg;
            break;
        case 'a':
            audio_enc.selected_encoder = encoder_selection_t::toolame_dab;
            break;
        case 'A':
            audio_enc.afterburner = false;
            break;
        case 'b':
            audio_enc.bitrate = std::stoi(optarg);
            break;
        case 'B':
            audio_enc.bandwidth = std::stoi(optarg);
            break;
        case 'c':
            audio_enc.channels = std::stoi(optarg);
            break;
        case 'd':
            audio_enc.alsa_device = optarg;
            break;
        case 'D':
            audio_enc.drift_compensation = true;
            break;
        case 'e':
            audio_enc.edi_output_uris.push_back(optarg);
            break;
        case 'T':
            audio_enc.tist_enabled = true;
            audio_enc.tist_delay_ms = std::stoi(optarg);
            break;
        case 'f':
            if (strcmp(optarg, "raw") == 0) {
                audio_enc.raw_input = 1;
            }
            else if (strcmp(optarg, "wav") != 0) {
                usage(argv[0]);
                return 1;
            }
            break;
        case 10:
            fprintf(stderr, "WARNING: the --vlc-gain option has been deprecated in favour of --audio-gain\n");
            // fallthrough
        case 'g':
            audio_enc.gain_dB = std::stod(optarg);
            break;
#ifdef HAVE_GST
        case 'G':
            audio_enc.gst_uri = optarg;
            break;
        case 11: // --gst-pipeline
            audio_enc.gst_pipeline = optarg;
            break;
#endif
        case 'i':
            audio_enc.infile = optarg;
            break;
        case 'j':
#if HAVE_JACK
            audio_enc.jack_name = optarg;
#else
            fprintf(stderr, "JACK disabled at compile time!\n");
            return 1;
#endif
            break;
        case 'k':
            audio_enc.keyfile = optarg;
            break;
        case 'l':
            audio_enc.show_level = 1;
            break;
        case 'o':
            audio_enc.output_uris.push_back(optarg);
            break;
        case 'p':
            audio_enc.padlen = std::stoi(optarg);
            break;
        case 'P':
            audio_enc.pad_ident = optarg;
            break;
        case 'r':
            audio_enc.sample_rate = std::stoi(optarg);
            break;
        case 'R':
            audio_enc.restart_on_fault = true;
            break;
        case 's':
            audio_enc.silence_timeout = std::stoi(optarg);
            if (audio_enc.silence_timeout > 0 && audio_enc.silence_timeout < 3600*24*30) {
                audio_enc.die_on_silence = true;
            }
            else {
                fprintf(stderr, "Invalid silence timeout (%d) given!\n", audio_enc.silence_timeout);
                return 1;
            }

            break;
        case 'S':
            audio_enc.send_stats_to = optarg;
            break;
        case 'w':
            audio_enc.icytext_file = optarg;
            break;
        case 'W':
            audio_enc.icytext_dlplus = true;
            break;
#ifdef HAVE_VLC
        case 'v':
            audio_enc.vlc_uri = optarg;
            break;
        case 'C':
            audio_enc.vlc_cache = optarg;
            break;
        case 'L':
            audio_enc.vlc_additional_opts.push_back(optarg);
            break;
#else
        case 'v':
            fprintf(stderr, "VLC input not enabled at compile time!\n");
            return 1;
#endif
        case 'V':
            audio_enc.verbosity++;
            break;
        case '?':
        case 'h':
            usage(argv[0]);
            return 1;
        }
    }

    if (not startupcheck.empty()) {
        etiLog.level(info) << "Running startup check '" << startupcheck << "'";
        int wstatus = system(startupcheck.c_str());

        if (WIFEXITED(wstatus)) {
            if (WEXITSTATUS(wstatus) == 0) {
                etiLog.level(info) << "Startup check ok";
            }
            else {
                etiLog.level(error) << "Startup check failed, returned " << WEXITSTATUS(wstatus);
                return 1;
            }
        }
        else {
            etiLog.level(error) << "Startup check failed, child didn't terminate normally";
            return 1;
        }
    }

    try {
        return audio_enc.run();
    }
    catch (const std::runtime_error& e) {
        fprintf(stderr, "ODR-AudioEnc failed to start: %s\n", e.what());
        return 1;
    }
}

